# Lever 6 - confine one dominant CPU consumer to half the cores # The mask stays until the process ends, so this is the consent tier. # # No double quotes and no semicolons are used anywhere in this file. The agent # script runner wraps content in double quotes, so an inner double quote ends # the wrapper early and any following text is run as a command. This file is # checked by resources/build/validate_script_quoting.py. $eventLogSource = 'HighCPURemediationConfineCores' $eventLogName = 'Application' if (-not [System.Diagnostics.EventLog]::SourceExists($eventLogSource)) { New-EventLog -LogName $eventLogName -Source $eventLogSource -ErrorAction SilentlyContinue } function Write-RemediationEvent { param([string]$Message) Write-Output $Message try { Write-EventLog -LogName $eventLogName -Source $eventLogSource -EntryType Information -EventId 1000 -Message $Message -ErrorAction Stop } catch { # Tolerated. Losing an audit line must not stop remediation itself. } } # CANONICAL protected list. Kept byte-identical with the copy in # HighCPURemediationCommon. Expanded into each script at build time because the # runner sends files standalone with no shared module state. Never hand-edit a # single copy. Drift that omits a security agent here would let this lever # core-confine your own EDR. # # The EDR block below (MsMpEng through SentinelStaticEngine) is the shared EDR # core and is kept identical, name for name, with the EDR block in # HighGPURemediationCommon's $neverTouchProcesses. resources/build/ # validate_protected_process_lists.py fails the build if either copy of this # full list or either tier's EDR core drifts. $protected = @( 'System','Idle','Registry','Memory Compression','smss','csrss','wininit', 'winlogon','services','lsass','svchost','dwm','explorer', 'MsMpEng','MsSense','MpDefenderCoreService','NisSrv','SecurityHealthService', 'CSFalconService','CSFalconContainer', 'SentinelAgent','SentinelServiceHost','SentinelStaticEngine', 'powershell','pwsh','conhost','wsmprovhost' ) $coreCount = [int](Get-CimInstance Win32_ComputerSystem -ErrorAction Stop).NumberOfLogicalProcessors if ($coreCount -lt 4) { Write-RemediationEvent ('skip: only {0} cores so halving leaves too little' -f $coreCount) return } # Above 64 cores Windows uses processor groups, which a single mask cannot # express, so this lever does not apply. if ($coreCount -gt 64) { Write-RemediationEvent ('skip: {0} cores means processor groups are in use' -f $coreCount) return } $before = @{} foreach ($p in (Get-Process -ErrorAction SilentlyContinue)) { try { $before[$p.Id] = $p.TotalProcessorTime.Ticks } catch { } } Start-Sleep -Seconds 2 $usage = @() foreach ($p in (Get-Process -ErrorAction SilentlyContinue)) { try { if (-not $before.ContainsKey($p.Id)) { continue } $cores = ($p.TotalProcessorTime.Ticks - $before[$p.Id]) / 20000000.0 if ($cores -lt 0) { continue } $usage += [pscustomobject]@{ Name = $p.ProcessName Id = $p.Id Cores = [math]::Round($cores, 2) } } catch { } } $hog = $usage | Where-Object { $protected -notcontains $_.Name } | Sort-Object Cores -Descending | Select-Object -First 1 $required = [math]::Round($coreCount * 0.7, 2) if ($null -eq $hog -or $hog.Cores -lt $required) { Write-RemediationEvent ('skip: no process holding {0} or more cores' -f $required) return } $p = Get-Process -Id $hog.Id -ErrorAction SilentlyContinue if ($null -eq $p) { return } $half = [int][math]::Floor($coreCount / 2) # The int64 cast matters. An int32 one shifted left by 32 wraps back to 1 and # yields a zero mask, which the kernel rejects. [int64]$mask = ([int64]1 -shl $half) - 1 [int64]$current = [int64]$p.ProcessorAffinity if ($current -eq $mask) { Write-RemediationEvent ('skip: {0} is already confined' -f $hog.Name) return } $p.ProcessorAffinity = [IntPtr]$mask Write-RemediationEvent ('confined: {0} PID {1} holding {2} cores limited to {3} of {4} cores. previous mask 0x{5}' -f $hog.Name, $hog.Id, $hog.Cores, $half, $coreCount, $current.ToString('X'))